package castledefense.logic;

import java.util.ArrayList;

import castledefense.data.BuildingData;
import castledefense.data.GameData;
import castledefense.data.UnitData;
import castledefense.game.screens.MainGameScreen;
import castledefense.logic.buildings.Building;
import castledefense.logic.buildings.Castle;
import castledefense.logic.buildings.EnemyBase;
import castledefense.logic.buildings.Tower1;
import castledefense.logic.buildings.Tower2;
import castledefense.logic.buildings.Wall;
import castledefense.logic.pathfinding.LinkForPathfinding;
import castledefense.logic.pathfinding.Pathfinding;
import castledefense.logic.units.MiniBoss;
import castledefense.logic.units.Mover;
import castledefense.logic.units.Unit;
import castledefense.sound.SoundHandler;
import castledefense.util.Log;

public class Map {
	public static final int cellWidth = 32;

	/**
	 * Variables to help decide where we can build on the map and to generate
	 * the movement map
	 */
	public static final int empty = 0;
	public static final int full = 1;
	public static final int unbuildable = 2;
	public static final int fire = 3;
	public static final int water = 4;
	public static final int earth = 5;
	public static final int air = 6;
	public static final int openGate = 7;
	public static final int wall = 8;

	protected int sizeX;
	protected int sizeY;
	protected Castle castle;
	protected MainGameScreen state;

	private int[][] constructionMap;
	private int[][] movementMap;

	protected ArrayList<Building> constructionList = new ArrayList<Building>();
	protected ArrayList<Building> constructionRemovedList = new ArrayList<Building>();
	protected ArrayList<Building> buildingList = new ArrayList<Building>(128);
	protected ArrayList<EnemyBase> enemyBaseList = new ArrayList<EnemyBase>(4);
	protected ArrayList<Unit> unitList = new ArrayList<Unit>(128);
	private ArrayList<Unit> unitRemoveList = new ArrayList<Unit>(8);
	private ArrayList<EnemyBase> enemyBaseRemoveList = new ArrayList<EnemyBase>(
			4);
	protected ArrayList<Building> selectedBuildingList = new ArrayList<Building>(
			1);
	protected ArrayList<Targetable> nearTargetsList = new ArrayList<Targetable>(
			4);
	protected Unit selectedUnit = null;
	private LinkForPathfinding link = new LinkForPathfinding();

	// ----------------------------- Update map class method
	// ------------------------------

	public void update(int deltaTime) {
		// Hack for build
		if (lastBuild > 0) {
			lastBuild -= deltaTime;
		}
		// End of hack
		link.updateBuilding();
		link.updateMover();
		for (Building building : constructionList) {
			if (isConstructable(building.getCellX(), building.getCellY(),
					building.getSize())) {
				addBuilding(building);
				constructionRemovedList.add(building);
			}
		}
		for (Building building : constructionRemovedList) {
			constructionList.remove(building);
		}

		for (Building building : buildingList) {
			building.update(deltaTime);
		}
		for (Unit unit : unitList) {
			unit.update(deltaTime);
		}
		for (Unit unit : unitRemoveList) {
			unitList.remove(unit);
		}
		unitRemoveList.clear();

		for (EnemyBase base : enemyBaseRemoveList) {
			removeBuilding(base);
			enemyBaseList.remove(base);
		}
		enemyBaseRemoveList.clear();
	}

	// -------------------------- Initialization -------------------------------
	public Map(int sizeX, int sizeY, MainGameScreen state) {
		this.sizeX = sizeX;
		this.sizeY = sizeY;
		this.state = state;
		// Construct the map
		constructionMap = new int[sizeX][sizeY];
		movementMap = new int[sizeX][sizeY];
		generateMap();
	}

	private void generateMap() {
		castle = new Castle(null, sizeX / 2 - 2, sizeY / 2 - 2,
				BuildingData.castle);
		// Test
		link.init((Map) this);
		// End of test

		addBasement(castle);

		// Place the 4 enemy bases
		Building tmp = new EnemyBase(this, 1, sizeY / 2 - 2,
				BuildingData.enemyBase, GameData.fire);
		addBasement(tmp);
		enemyBaseList.add((EnemyBase) tmp);

		tmp = new EnemyBase(this, sizeX - 5, sizeY / 2 - 2,
				BuildingData.enemyBase, GameData.water);
		addBasement(tmp);
		enemyBaseList.add((EnemyBase) tmp);

		tmp = new EnemyBase(this, sizeX / 2, 1, BuildingData.enemyBase,
				GameData.earth);
		addBasement(tmp);
		enemyBaseList.add((EnemyBase) tmp);

		tmp = new EnemyBase(this, sizeX / 2, sizeY - 5, BuildingData.enemyBase,
				GameData.air);
		addBasement(tmp);
		enemyBaseList.add((EnemyBase) tmp);

		// Make area around the map unbuildable
		for (int i = 0; i < sizeX; i++) {
			setConstructionMapValue(i, 0, unbuildable);
			setConstructionMapValue(i, sizeY - 1, unbuildable);
		}
		for (int j = 0; j < sizeY; j++) {
			setConstructionMapValue(0, j, unbuildable);
			setConstructionMapValue(sizeX - 1, j, unbuildable);
		}
		for (int i = 0; i < sizeX; i++) {
			setConstructionMapValue(i, 0, unbuildable);
			setConstructionMapValue(i, sizeY - 1, unbuildable);
		}
		for (int j = 0; j < sizeY; j++) {
			setConstructionMapValue(0, j, unbuildable);
			setConstructionMapValue(sizeX - 1, j, unbuildable);
		}

		// Add wall around the map
		for (int i = 0; i < 200; i++) {
			int x = (int) (Math.random() * sizeX);
			int y = (int) (Math.random() * sizeY);
			if (isBuildable(x, y, 2)) {
				addBasement(new Wall(this, x, y));
			}

		}
	}

	// --------------------------- Getter and setter
	// ------------------------------
	public Castle getCastle() {
		return castle;
	}

	public ArrayList<Building> getConstructionList() {
		return constructionList;
	}

	public ArrayList<Building> getBuildingList() {
		return buildingList;
	}

	public ArrayList<Unit> getUnitList() {
		return unitList;
	}

	public int getSizeX() {
		return sizeX;
	}

	public int getSizeY() {
		return sizeY;
	}

	public int getConstructionMapValue(int x, int y) {
		return constructionMap[x][y];
	}

	public void setConstructionMapValue(int x, int y, int value) {
		constructionMap[x][y] = value;
	}

	// --------------------------- Pathfinding methods
	// ----------------------------

	public double getMovementValue(int cellX, int cellY, int type) {
		return Pathfinding.getMovementValue(movementMap[cellX][cellY], type);
	}

	private void setMovementValue(int x, int y, int value) {
		movementMap[x][y] = value;
	}

	private void setMovementValue(Building building) {
		int x = building.getCellX();
		int y = building.getCellY();
		int size = building.getSize();
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				if (building instanceof Castle || building instanceof EnemyBase) {
					setMovementValue(i, j, unbuildable);
				} else if (building instanceof Wall) {
					setMovementValue(i, j, wall);
				} else {
					setMovementValue(i, j, full);
				}
			}
		}
	}

	public void recalculatePath(Mover mover) {
		link.addMover(mover);
	}

	// --------------------------- Select unit method
	// --------------------------

	public Unit selectUnit(float x, float y) {
		unSelectUnit();
		for (Unit unit : unitList) {
			if (unit.biggerContains(x, y)) {
				selectedUnit = unit;
				selectedUnit.setSelected(true);
				unSelectBuildings();
				return selectedUnit;
			}
		}
		return null;
	}

	// --------------------------- UnSelects unit method
	// ------------------------
	public void unSelectUnit() {
		if (selectedUnit != null) {
			selectedUnit.setSelected(false);
			selectedUnit = null;
		}
	}

	// --------------------------- Select building method
	// --------------------------

	public Building selectBuilding(int x, int y) {
		unSelectBuildings();

		for (Building building : buildingList) {
			if (building.contains(x, y)) {
				selectedBuildingList.add(building);
				building.setSelected(true);
				unSelectUnit();
				return building;
			}
		}
		return null;
	}

	// --------------------------- UnSelects building method
	// ------------------------
	public void unSelectBuildings() {
		for (Building building : selectedBuildingList) {
			building.setSelected(false);
		}
		selectedBuildingList.clear();
	}

	// --------------------------- Add building
	// methods-----------------------------

	public boolean isBuildable(int x, int y, int size) {
		// Check if it is in the map
		if (!(x >= 0 && x + size <= sizeX && y >= 0 && y + size <= sizeY)) {
			return false;
		}
		boolean result = true;
		// Check if the space is empty
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				result &= (getConstructionMapValue(i, j) == empty);
			}
		}
		return result;
	}

	public boolean isConstructable(int x, int y, int size) {
		boolean result = true;
		// Check if no unit are in the way
		for (int i = 0; i < unitList.size(); i++) {
			if (unitList.get(i).isContains(x, y, size)) {
				result = false;
			}
		}
		return result;
	}

	private int timeBetweenBuild = 300;
	private int lastBuild = 0;

	public void addBasement(Building building) {
		if (lastBuild > 0) {
			return;
		}
		int x = building.getCellX();
		int y = building.getCellY();
		int size = building.getSize();
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				if (building instanceof Castle || building instanceof EnemyBase) {
					setConstructionMapValue(i, j, unbuildable);
				} else if (building instanceof Wall) {
					setConstructionMapValue(i, j, wall);
				} else {
					setConstructionMapValue(i, j, full);
				}
			}
		}
		// Check path for every elements
		link.setConstructionMapBasement(building);
		link.addBuilding(building);
	}

	public void addConstruction(Building building) {
		BuildingData.money -= building.getCost();
		constructionList.add(building);
		// link.setConstructionMap(building.createBasement()); //TODO
	}

	public void addBuilding(Building building) {

		if (building instanceof Tower1) {
			SoundHandler.buildingSound();
			Tower1 tower = (Tower1) building;
			tower.setMap((Map) this);
		} else if (building instanceof Tower2) {
			SoundHandler.buildingSound();
			Tower2 tower = (Tower2) building;
			tower.setMap((Map) this);
		}

		// Update unit movement map
		link.setConstructionMap(building); // New unit will move away from this
											// building

		setMovementValue(building);

		// this hackery add building to right place to for nice z ordering
		final int buildingListSize = buildingList.size();
		Log.k1(buildingListSize);
		Log.timerStart();
		int index = 0;
		while (index < buildingListSize
				&& building.getCellY() < buildingList.get(index).getCellY()) {
			index++;
		}		
		buildingList.add(index, building);
		Log.timerStop();
	}

	public void removeBasementPath(Building building) {
		int x = building.getCellX();
		int y = building.getCellY();
		int size = building.getSize();
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				setConstructionMapValue(i, j, empty);
			}
		}
		link.setConstructionMapBasement(building);
		lastBuild = timeBetweenBuild;
	}

	public boolean hasBuilding(int x, int y) {
		// Check if it is in the map
		if (x < 0 || y < 0 || x >= sizeX || y >= sizeY) {
			return false;
		}
		return getConstructionMapValue(x, y) != empty
				&& getConstructionMapValue(x, y) != unbuildable;
	}

	// --------------------------- Remove building
	// methods-----------------------------

	public int getSizeBuilding(int x, int y) {
		for (int i = 0; i < buildingList.size(); i++) {
			Building building = buildingList.get(i);
			int posX = building.getCellX();
			int posY = building.getCellY();
			int size = building.getSize();
			if (x >= posX && x < posX + size && y >= posY && y < posY + size) {
				return building.getSize();
			}
		}
		// Should never reach here
		return 1;
	}

	public int getPosXBuilding(int x, int y) {
		for (int i = 0; i < buildingList.size(); i++) {
			Building building = buildingList.get(i);
			int posX = building.getCellX();
			int posY = building.getCellY();
			int size = building.getSize();
			if (x >= posX && x < posX + size && y >= posY && y < posY + size) {
				return building.getCellX();
			}
		}
		// Should never reach here
		return 1;
	}

	public int getPosYBuilding(int x, int y) {
		for (int i = 0; i < buildingList.size(); i++) {
			Building building = buildingList.get(i);
			int posX = building.getCellX();
			int posY = building.getCellY();
			int size = building.getSize();
			if (x >= posX && x < posX + size && y >= posY && y < posY + size) {
				return building.getCellY();
			}
		}
		// Should never reach here
		return 1;
	}

	public void destroyBuilding(int x, int y) {
		for (int i = 0; i < buildingList.size(); i++) {
			Building building = buildingList.get(i);
			int posX = building.getCellX();
			int posY = building.getCellY();
			int size = building.getSize();
			if (x >= posX && x < posX + size && y >= posY && y < posY + size) {
				BuildingData.money += (3 * building.getCost()) / 4;
				removeBuilding(building);
				return;
			}
		}
	}

	private void removeBuilding(Building building) {
		state.getStateGUI().getUpgradePanel().nullWhichInfoIfSelected(building);
		buildingList.remove(building);
		int x = building.getCellX();
		int y = building.getCellY();
		int size = building.getSize();
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				setConstructionMapValue(i, j, empty);
				setMovementValue(i, j, empty);
			}
		}
		link.setConstructionMap(building);
		link.setConstructionMapBasement(building);
	}

	public void updateBuilding(Building building, int newType) {
		int x = building.getCellX();
		int y = building.getCellY();
		int size = building.getSize();
		for (int i = x; i < x + size; i++) {
			for (int j = y; j < y + size; j++) {
				setConstructionMapValue(i, j, newType);
			}
		}

		link.addBuilding(building);
		link.setConstructionMap(building);

	}

	// ------------------------------ Add remove unit methods
	// -------------------------

	public void addUnit(Unit unit) {
		unitList.add(unit);
		link.addMover(unit);
	}

	public void removeUnit(Unit unit) {
		unitRemoveList.add(unit);
		link.removeMover(unit);
	}

	// ---------------------------- Tower attack unit methods
	// --------------------------

	public boolean contains(Unit unit) {
		return unitList.contains(unit);
	}

	public Targetable getNearestUnit(double x, double y, double radius) {
		final double r2 = radius * radius;

		int size = enemyBaseList.size();
		for (int i = 0; i < size; i++) {
			EnemyBase target = enemyBaseList.get(i);
			final double dx = target.getCenterX() - x;
			final double dy = target.getCenterY() - y;
			if (r2 >= (dx * dx + dy * dy)) {
				return target;
			}
		}

		size = unitList.size();
		for (int i = 0; i < size; i++) {
			Unit unit = unitList.get(i);
			final double dx = unit.getCenterX() - x;
			final double dy = unit.getCenterY() - y;
			if (r2 >= (dx * dx + dy * dy)) {
				return unit;
			}
		}

		return null;
	}

	public ArrayList<Targetable> getNearUnits(double x, double y,
			double radius, int howMany) {
		nearTargetsList.clear();
		final double r2 = radius * radius;

		int size = enemyBaseList.size();
		for (int i = 0; i < size; i++) {
			EnemyBase target = enemyBaseList.get(i);
			double dx = target.getCenterX() - x;
			double dy = target.getCenterY() - y;
			if (r2 >= (dx * dx + dy * dy)) {
				nearTargetsList.add(target);
				howMany--;
				if (howMany <= 0) {
					return nearTargetsList;
				}
			}
		}
		size = unitList.size();
		for (int i = 0; i < size; i++) {
			Unit unit = unitList.get(i);
			double dx = unit.getCenterX() - x;
			double dy = unit.getCenterY() - y;
			if (r2 >= (dx * dx + dy * dy)) {
				nearTargetsList.add(unit);
				howMany--;
				if (howMany <= 0) {
					return nearTargetsList;
				}
			}
		}
		return nearTargetsList;
	}

	public void removeEnemyBase(EnemyBase enemyBase) {
		enemyBaseRemoveList.add(enemyBase);
		UnitData.addAura(enemyBase.getElementType());
		MiniBoss boss = new MiniBoss((int) enemyBase.getCenterX(),
				(int) enemyBase.getCenterY(), enemyBase.getElementType(), this);
		this.addUnit(boss);
	}

}
